Skip to content

fix: migrate extension commands on integration switch#2404

Merged
mnriem merged 2 commits intogithub:mainfrom
cyliu0:fix/integration-switch-extension-commands
May 1, 2026
Merged

fix: migrate extension commands on integration switch#2404
mnriem merged 2 commits intogithub:mainfrom
cyliu0:fix/integration-switch-extension-commands

Conversation

@cyliu0
Copy link
Copy Markdown
Contributor

@cyliu0 cyliu0 commented Apr 29, 2026

Description

When switching integrations (e.g., specify integration switch opencode), extension commands were not re-registered for the new agent. This caused two problems:

  1. Missing commands: The new agent's directory did not receive extension commands (e.g., git extension commands like speckit.git.feature were absent after switching from kimi to opencode)
  2. Orphaned files: Extension files remained in the old agent's directory with stale registry entries pointing to the old agent

Root Cause

Extension commands are registered during specify extension add via CommandRegistrar.register_commands_for_all_agents(), which writes commands for all agents that already have directories. When switching integrations later, the new agent gets a fresh directory but extensions are never re-registered.

Fix

Added two new ExtensionManager methods:

  1. unregister_agent_artifacts(agent_name) — Called after uninstalling the old integration. Removes extension command files and skills for the old agent, and cleans up registry entries.

  2. register_enabled_extensions_for_agent(agent_name) — Called after installing the new integration. Re-registers all enabled extensions for the new agent. Correctly handles Copilot --skills mode by detecting ai_skills in init-options and skipping command registration when skills mode is active.

Wired both into integration_switch() in src/specify_cli/__init__.py.

Testing

  • Tested locally with uv run specify --help
  • Ran existing tests with uv sync --extra test && uv run pytest
  • Tested with a sample project (see below)

Automated tests added

  • test_switch_migrates_extension_commands: Tests kimi → opencode → claude migration, verifying extension commands appear for the new agent and are removed from the old
  • test_switch_migrates_copilot_skills_extension_commands: Tests Copilot with --skills flag to ensure extensions are installed as skills, not commands
  • test_switch_does_not_register_disabled_extensions: Ensures disabled extensions are not re-registered after switch

Manual test results

Agent: Manual CLI testing | OS/Shell: macOS/zsh

Command tested Notes
specify init . --integration kimi Git extension skills created in .kimi/skills/
specify integration switch opencode Extension commands migrated to .opencode/command/, old skills removed
specify integration switch claude Extension skills migrated to .claude/skills/, old commands removed

Test results

tests/integrations/test_integration_subcommand.py: 28 passed
tests/test_extensions.py: 190 passed
tests/integrations/: 918 passed

AI Disclosure

  • I did not use AI assistance for this contribution
  • I did use AI assistance (describe below)

This fix was developed with assistance from OpenAI Codex and Kimi coding agents. AI was used for:

  • Initial code exploration and root cause analysis
  • Implementation of the unregister_agent_artifacts and register_enabled_extensions_for_agent methods
  • Test case generation and verification

All changes were reviewed, tested, and validated manually before submission.

@cyliu0 cyliu0 requested a review from mnriem as a code owner April 29, 2026 12:17
Copilot AI review requested due to automatic review settings April 29, 2026 12:17
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes extension command/skill migration when switching integrations so extension artifacts are cleaned up for the old agent and re-registered for the new agent, keeping agent directories and the extension registry consistent.

Changes:

  • Added ExtensionManager.unregister_agent_artifacts() to remove old agent extension command files and extension-generated skills, and to update the registry accordingly.
  • Added ExtensionManager.register_enabled_extensions_for_agent() to re-register all enabled extensions for the new integration, including Copilot --skills mode behavior.
  • Wired both steps into integration_switch() and added integration-switch tests covering migration, skills mode, and disabled extensions.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
src/specify_cli/__init__.py Calls extension cleanup before clearing metadata and re-registration after installing the target integration.
src/specify_cli/extensions.py Implements agent artifact unregistration and enabled-extension re-registration logic.
tests/integrations/test_integration_subcommand.py Adds regression tests ensuring artifacts migrate/clean correctly across integrations and modes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/extensions.py Outdated
Comment thread src/specify_cli/extensions.py Outdated
@cyliu0 cyliu0 force-pushed the fix/integration-switch-extension-commands branch from 4b9719a to 591c6a4 Compare April 29, 2026 12:34
@cyliu0
Copy link
Copy Markdown
Contributor Author

cyliu0 commented Apr 29, 2026

Thanks for the review! All three comments have been addressed in the latest commit:

  1. Redundant inner if installed_key check (src/specify_cli/init.py): Removed the redundant inner check. The outer block already guarantees installed_key is truthy.

  2. commands_dir.is_dir() guard (src/specify_cli/extensions.py): Removed the guard and replaced it with commands_dir.mkdir(parents=True, exist_ok=True) so extensions are always re-registered on switch, even if the directory does not yet exist.

  3. Agent-scoped skills cleanup (src/specify_cli/extensions.py): Fixed unregister_agent_artifacts to explicitly resolve the skills directory for the provided agent_name using the same _get_skills_dir helper that extension install uses, rather than relying on the currently-active agent in init-options. Also updated _unregister_extension_skills to accept an optional skills_dir parameter.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 3/3 changed files
  • Comments generated: 3

Comment thread tests/integrations/test_integration_subcommand.py Outdated
Comment thread src/specify_cli/extensions.py Outdated
Comment thread src/specify_cli/__init__.py Outdated
@mnriem
Copy link
Copy Markdown
Collaborator

mnriem commented Apr 29, 2026

Please address Copilot feedback. If not applicable, please explain why

@cyliu0 cyliu0 force-pushed the fix/integration-switch-extension-commands branch from 591c6a4 to fe788ab Compare April 29, 2026 14:17
@cyliu0
Copy link
Copy Markdown
Contributor Author

cyliu0 commented Apr 29, 2026

Thanks for the second round of review! All three comments have been addressed:

  1. Refactored test_switch_migrates_extension_commands to use the existing _run_in_project() helper instead of repeated os.chdir()/try/finally blocks, keeping the test consistent with others in the file.

  2. Removed the redundant commands_dir.mkdir() call in register_enabled_extensions_for_agent() since CommandRegistrar.register_commands_for_agent() already creates the directory.

  3. Updated the warning message in integration_switch() to mention both commands and skills: Could not register extension commands, skills, or related artifacts.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 4 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/specify_cli/extensions.py
Comment thread tests/integrations/test_integration_subcommand.py
@cyliu0 cyliu0 force-pushed the fix/integration-switch-extension-commands branch from fe788ab to 1183d35 Compare April 29, 2026 14:39
@cyliu0
Copy link
Copy Markdown
Contributor Author

cyliu0 commented Apr 29, 2026

Addressing the two new comments:

Comment 7 (Copilot skills mode frontmatter): The concern about mode: mapping is already handled correctly. CopilotIntegration.post_process_skill_content() parses the name: field from frontmatter and converts speckit-git-feature back to speckit.git-feature format before injecting mode:. Verified locally that generated SKILL.md contains mode: speckit.git-feature.

Comment 8 (Test verification): Added assertion in test_switch_migrates_copilot_skills_extension_commands to verify the generated SKILL.md contains the correct mode: field mapping.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 3/4 changed files
  • Comments generated: 2

Comment thread src/specify_cli/extensions.py
Comment thread src/specify_cli/extensions.py
@cyliu0 cyliu0 force-pushed the fix/integration-switch-extension-commands branch from 1183d35 to 788be6e Compare April 29, 2026 15:08
@cyliu0
Copy link
Copy Markdown
Contributor Author

cyliu0 commented Apr 29, 2026

Addressing the two new comments:

Comment 9 (Unknown agent cleanup): Added a guard in unregister_agent_artifacts() to return early when agent_name is not present in CommandRegistrar.AGENT_CONFIGS. This prevents losing registry entries while leaving orphaned files on disk during the "unknown integration via manifest" switch path.

Comment 10 (Skills directory fallback): Changed the skills_dir passed to _unregister_extension_skills() to be None when the resolved agent_skills_dir does not exist on disk. This re-enables the fallback scan of all known agent skills directories, which is important for cleaning up stale registered_skills entries created by earlier installs.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 3/4 changed files
  • Comments generated: 1

Comment thread src/specify_cli/extensions.py Outdated
@mnriem
Copy link
Copy Markdown
Collaborator

mnriem commented Apr 29, 2026

Please address Copilot feedback

@cyliu0 cyliu0 force-pushed the fix/integration-switch-extension-commands branch from 788be6e to f05638c Compare April 30, 2026 06:32
@cyliu0
Copy link
Copy Markdown
Contributor Author

cyliu0 commented Apr 30, 2026

Addressing the latest Copilot comment (comment #11):

unregister_agent_artifacts() was unconditionally clearing registered_skills after calling _unregister_extension_skills(). However, _unregister_extension_skills() may intentionally skip deletion when it cannot verify ownership via metadata.source (e.g., corrupted/missing SKILL.md). This left skill directories on disk while removing the only registry reference needed for future cleanup.

Fixed by checking which skill directories still exist after cleanup and only keeping those still-present names in the registry. Future cleanup attempts will still be able to find skipped skills.

@cyliu0
Copy link
Copy Markdown
Contributor Author

cyliu0 commented Apr 30, 2026

@mnriem All Copilot feedback has been addressed across multiple commits. Here's a summary of all 11 comments and their resolutions:

  1. ✅ Redundant inner if installed_key check - removed
  2. ✅ commands_dir.is_dir() guard preventing re-registration - replaced with mkdir()
  3. ✅ Agent-scoped skills cleanup - now uses _get_skills_dir() for the specific agent
  4. ✅ Test using manual os.chdir() - refactored to use _run_in_project() helper
  5. ✅ Redundant commands_dir mkdir() - removed (registrar handles it)
  6. ✅ Warning message accuracy - updated to mention skills/artifacts
  7. ✅ Copilot skills mode: frontmatter mapping - verified correct, added test assertion
  8. ✅ Test missing mode: assertion - added assertion for mode: field
  9. ✅ Unknown agent cleanup guard - added AGENT_CONFIGS check to skip unsupported agents
  10. ✅ Skills directory fallback - pass None when dir doesn't exist to enable fallback scan
  11. ✅ Unconditional registered_skills clearing - now only removes entries for actually-deleted skills

All 244 tests pass. Please re-review when you have a chance.

@cyliu0 cyliu0 requested a review from Copilot April 30, 2026 06:39
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 4 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/specify_cli/extensions.py
Comment thread src/specify_cli/extensions.py Outdated
@cyliu0 cyliu0 force-pushed the fix/integration-switch-extension-commands branch from f05638c to fed6c19 Compare April 30, 2026 06:48
@cyliu0
Copy link
Copy Markdown
Contributor Author

cyliu0 commented Apr 30, 2026

Addressing the two new Copilot comments (round 4):

Comment 12 (Stale registry on empty registration): register_enabled_extensions_for_agent() now explicitly removes the agent entry from registered_commands when registration returns an empty list (e.g., corrupted manifest pointing at missing command files). This prevents stale metadata that later cleanup would try to remove.

Comment 13 (Incorrect registry reconciliation during fallback cleanup): When skills_dir is None (fallback scan mode), we no longer use agent_skills_dir to check remaining skills. Registry reconciliation now only happens when skills_dir is explicitly set (specific directory targeted), correctly matching what _unregister_extension_skills() actually removed.

All 220 tests pass (30 integration subcommand + 190 extension tests).

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 4 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/specify_cli/extensions.py Outdated
@cyliu0 cyliu0 force-pushed the fix/integration-switch-extension-commands branch from fed6c19 to 9b4e87a Compare April 30, 2026 07:03
@cyliu0
Copy link
Copy Markdown
Contributor Author

cyliu0 commented Apr 30, 2026

Fixed comment 14: Changed registry reconciliation to check (skills_dir / skill_name).is_dir() instead of SKILL.md existence. When _unregister_extension_skills() skips deletion due to missing SKILL.md (leaving directory in place), the previous check would incorrectly drop the skill from registered_skills. Now we check directory existence so skipped skills remain in the registry for future cleanup attempts.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 4 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/specify_cli/extensions.py
Comment thread src/specify_cli/__init__.py
When switching integrations (e.g. kimi → opencode), extension commands
were not re-registered for the new agent, leaving the new agent without
extension support and orphaning files in the old agent's directory.

Changes:
- Add ExtensionManager.unregister_agent_artifacts() to clean up old
  agent extension files and registry entries during switch
- Add ExtensionManager.register_enabled_extensions_for_agent() to
  re-register all enabled extensions for the new agent
- Wire both into integration_switch() after uninstall/install phases
- Handle skills mode (Copilot --skills) correctly
- Add tests for kimi→opencode→claude migration, Copilot skills mode,
  and disabled extension handling

Fixes extension commands not appearing after integration switch.
@cyliu0 cyliu0 force-pushed the fix/integration-switch-extension-commands branch from 9b4e87a to f2a929d Compare April 30, 2026 07:25
@cyliu0
Copy link
Copy Markdown
Contributor Author

cyliu0 commented Apr 30, 2026

Fixed: Updated the warning message to refer to "extension artifacts (commands, skills, registry entries)" instead of just "extension commands", since unregister_agent_artifacts() also removes skills and updates registry entries.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 4 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/specify_cli/extensions.py Outdated
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 30, 2026 07:41
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 4 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 3/4 changed files
  • Comments generated: 0 new

@mnriem mnriem merged commit 5edc9a5 into github:main May 1, 2026
18 of 19 checks passed
@mnriem
Copy link
Copy Markdown
Collaborator

mnriem commented May 1, 2026

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants